package cn.edu.cug.cs.gtl.oi.utils.editors;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;

import com.openinventor.inventor.SbColor;
import com.openinventor.inventor.SbRotation;
import com.openinventor.inventor.SbViewportRegion;
import com.openinventor.inventor.SoDB;
import com.openinventor.inventor.SoInput;
import com.openinventor.inventor.SoPath;
import com.openinventor.inventor.actions.SoSearchAction;
import com.openinventor.inventor.draggers.SoDragger;
import com.openinventor.inventor.manips.SoDirectionalLightManip;
import com.openinventor.inventor.misc.callbacks.SoDraggerCB;
import com.openinventor.inventor.nodes.*;
import com.openinventor.inventor.sensors.SoNodeSensor;
import com.openinventor.inventor.viewercomponents.awt.IRenderAreaInteractive;
import com.openinventor.inventor.viewercomponents.awt.tools.SliderPanel;
import com.openinventor.inventor.viewercomponents.nodes.SceneInteractor;

import cn.edu.cug.cs.gtl.oi.utils.ViewerComponentsFactory;

/**
 * This class is used to edit an <B>SoDirectionalLight</B> node (color,
 * intensity and direction are changed). In addition to directly editing
 * directional light nodes, the editor can also be used with listeners which
 * will be called whenever the light is changed. The component consists of a
 * render area and a value slider in the main window, with controls to display a
 * color picker. In the render area there appears a sphere representing the
 * world, and a directional light manipulator representing the direction of the
 * light. Picking on the manipulator and moving the mouse provides direct
 * manipulation of the light direction. The picker is used to edit the color,
 * and the value slider edits the intensity. <br>
 * <br>
 * The editor can currently be attached to only one light at a time. Attaching
 * to two different lights will automatically detach the first one before
 * attaching the second.
 *
 */
public class DirectionalLightEditor extends JFrame
{
  // the menu items
  private static final int k_COLOR_EDITOR = 0;
  private static final int k_CLOSE = 1;

  private JMenuBar menubar;
  private JMenu menuEdit;
  private JMenuItem menuColorEditor, menuClose;

  private CameraSensorTask cameraSensorTask;

  protected SoDirectionalLight dirLight; // light we are editing
  protected SoSeparator root; // root of local scene graph
  protected SoSeparator litStuff; // what's on display under the light

  protected SoCamera cameraToWatch;

  protected ColorEditor colorEditor;
  protected SliderPanel intensitySlider;

  protected IRenderAreaInteractive renderArea;
  protected SoNodeSensor lightSensor;
  protected SoNodeSensor cameraSensor;
  protected SoDirectionalLightManip dirLightManip;
  protected boolean ignoreCallback; // TRUE while callback should be ignored
  protected List<Listener> editorListeners;

  // @formatter:off
  protected static String geomBuffer =
  "#Inventor V2.0 ascii\n" +
  "Separator {\n" +
  "  DEF DIR_LIGHT_EDITOR_ROTATOR Group {\n" +
  "	   LightModel { model PHONG }\n" +
  "	   Separator {\n" +
  "	     Transform {\n" +
  "		   scaleFactor .2 .2 .2\n" +
  "		   rotation    1 0 0 -1.57079632679489661923  # PI/2\n" +
  "		   translation 0 0 1.2\n" +
  "	     }\n" +
  "	     Cone {}\n" +
  "	   }\n" +
  "	   Separator {\n" +
  "	     Transform {\n" +
  "		   scaleFactor .08 .3 .08\n" +
  "		   rotation    1 0 0 -1.57079632679489661923  # PI/2\n" +
  "		   translation 0 0 1.7\n" +
  "	     }\n" +
  "	     Cylinder {}\n" +
  "	   }\n" +
  "	   Separator {\n" +
  "	     Transform {\n" +
  "		   scaleFactor .1 .1 .1\n" +
  "		   rotation    1 0 0 -1.57079632679489661923  # PI/2\n" +
  "		   translation 0 0 -1.1\n" +
  "	     }\n" +
  "	     Cone {}\n" +
  "	   }\n" +
  "  }\n" +
  "  DEF dirLightEditorRotator Separator {\n" +
  "	   Material {\n" +
  "	     diffuseColor	[ 0.5 0.5 0.5 ]\n" +
  "	     emissiveColor	[ 0.5 0.5 0.5 ]\n" +
  "	   }\n" +
  "	   DrawStyle { lineWidth 2 }\n" +
  "	   USE DIR_LIGHT_EDITOR_ROTATOR\n" +
  "  }\n" +
  "  DEF dirLightEditorRotatorActive Separator {\n" +
  "	   Material {\n" +
  "	     diffuseColor	[ 0.5 0.5 0.0 ]\n" +
  "	     emissiveColor	[ 0.5 0.5 0.0 ]\n" +
  "	   }\n" +
  "	   DrawStyle { lineWidth 3 }\n" +
  "	   USE DIR_LIGHT_EDITOR_ROTATOR\n" +
  "  }\n" +
  "}\n";
  // @formatter:on

  public DirectionalLightEditor()
  {
    setTitle("Directional Light");

    addComponentListener(new ListenVisibility());
    dirLight = null;
    editorListeners = new ArrayList<DirectionalLightEditor.Listener>();

    // create the dir light manip our own geom
    SoInput in = new SoInput();
    in.setBuffer(geomBuffer.getBytes());
    SoDB.readNode(in);
    dirLightManip = new SoDirectionalLightManip();

    // set the other parts to be our arrow.
    SoDragger liteDragger = dirLightManip.getDragger();
    SoNode from = SoNode.getByName("dirLightEditorRotator");
    if ( from != null )
      liteDragger.setPart("rotator.rotator", from);
    from = SoNode.getByName("dirLightEditorRotatorActive");
    if ( from != null )
      liteDragger.setPart("rotator.rotatorActive", from);

    // set the other parts to null
    SoSeparator dummySep = new SoSeparator();
    liteDragger.setPart("translator.xTranslator.translator", dummySep);
    liteDragger.setPart("translator.yTranslator.translator", dummySep);
    liteDragger.setPart("translator.zTranslator.translator", dummySep);
    liteDragger.setPart("translator.xTranslator.translatorActive", dummySep);
    liteDragger.setPart("translator.yTranslator.translatorActive", dummySep);
    liteDragger.setPart("translator.zTranslator.translatorActive", dummySep);
    liteDragger.setPart("translator.yzTranslator.translator", dummySep);
    liteDragger.setPart("translator.xzTranslator.translator", dummySep);
    liteDragger.setPart("translator.xyTranslator.translator", dummySep);
    liteDragger.setPart("translator.yzTranslator.translatorActive", dummySep);
    liteDragger.setPart("translator.xzTranslator.translatorActive", dummySep);
    liteDragger.setPart("translator.xyTranslator.translatorActive", dummySep);
    liteDragger.setPart("rotator.feedback", dummySep);
    liteDragger.setPart("rotator.feedbackActive", dummySep);

    liteDragger.addValueChangedCallback(new DirLightManipCB(), this);

    // null out components. they'll be created in buildGUI().
    colorEditor = null;
    intensitySlider = null;
    renderArea = null;

    // Callbacks
    ignoreCallback = false;

    // set up the light sensor
    // this tells us if someone else changed the light
    lightSensor = new SoNodeSensor(new LightSensorTask());

    // set up the camera sensor - this will keep our camera oriented to
    // match the scene the light is in.
    cameraSensor = new SoNodeSensor();
    cameraSensorTask = new CameraSensorTask();
    cameraSensor.setTask(cameraSensorTask);
    cameraToWatch = null;

    // local scene graph for direct manipulation
    root = new SoSeparator();
    litStuff = new SoSeparator();
    SoEnvironment environ = new SoEnvironment();
    SoMaterial material = new SoMaterial();
    SoComplexity complexity = new SoComplexity();
    SoSphere sphere = new SoSphere();

    litStuff.addChild(environ);
    litStuff.addChild(material);
    litStuff.addChild(complexity);
    litStuff.addChild(sphere);

    root.addChild(dirLightManip);
    root.addChild(litStuff);

    // some ambient light
    environ.ambientColor.setValue(1, 1, 1);
    environ.ambientIntensity.setValue(0.5f);

    // let's have an interesting material !
    material.ambientColor.setValue(.2f, .2f, .2f);
    material.diffuseColor.setValue(.55f, .55f, .55f);
    material.specularColor.setValue(.70f, .70f, .70f);
    material.shininess.setValue(1);

    // and some complexity
    complexity.value.setValue(.6f);

    buildGUI();

    // unmap on window manager close button
    WindowListener l = new WindowAdapter()
    {
      @Override
      public void windowClosing(WindowEvent e)
      {
        setVisible(false);
      }
    };
    addWindowListener(l);

  }

  @Override
  protected void finalize() throws Throwable
  {
    if ( isAttached() )
      detach();
  }

  private JMenuBar buildMenuBar()
  {
    menubar = new JMenuBar();
    menuEdit = new JMenu("Edit");
    menubar.add(menuEdit);

    menuColorEditor = new JMenuItem("Color Editor");
    menuClose = new JMenuItem("Close");

    menuEdit.add(menuColorEditor);
    menuEdit.addSeparator();
    menuEdit.add(menuClose);

    menuColorEditor.addActionListener(new ListenMenuItem(k_COLOR_EDITOR));
    menuClose.addActionListener(new ListenMenuItem(k_CLOSE));

    return menubar;
  }

  private void buildGUI()
  {
    intensitySlider = new SliderPanel(0.f, 1.f, 0.f, 2);
    intensitySlider.addInfoText("Intensity");
    intensitySlider.addSliderPanelListener(new ListenIntensitySlider());
    intensitySlider.setSliderSize(new Dimension(100, 40));

    renderArea = ViewerComponentsFactory.createRenderAreaInteractive();
    renderArea.setSceneGraph(root);

    final Component canvas = renderArea.getComponent();
    canvas.setSize(150, 150);
    SceneInteractor sceneRoot = renderArea.getSceneInteractor();
    sceneRoot.enableHeadlight(false);
    sceneRoot.getCamera().viewAll(litStuff, new SbViewportRegion(canvas.getSize()), 2);

    menubar = buildMenuBar();

    setJMenuBar(menubar);

    setLayout(new BorderLayout());
    add(canvas, BorderLayout.CENTER);
    add(intensitySlider, BorderLayout.SOUTH);
    pack();
  }

  /**
   * Edit a directional light node.
   */
  public void attach(SoPath pathToLight)
  {
    if ( isAttached() )
      detach();

    if ( pathToLight == null )
      return;

    if ( pathToLight.regular.getTail() instanceof SoDirectionalLight )
    {
      dirLight = (SoDirectionalLight) pathToLight.regular.getTail();

      // set values in dirLightManip
      ignoreCallback = true; // ignore resulting manip callback
      copyLight(dirLightManip, dirLight); // dst, src
      ignoreCallback = false;

      // if the path contains a camera, put a sensor on camera
      // to keep our view the same as the
      // view of the light
      SoSearchAction sa = new SoSearchAction();
      sa.setNodeClass(SoCamera.class, true);
      // sa.apply(fullPathToLight) ;
      sa.apply(pathToLight);
      if ( sa.getPath() != null )
      {
        cameraToWatch = (SoCamera) sa.getPath().regular.getTail();
        cameraSensorTask.run();
      }
      else
      {
        if ( cameraToWatch != null )
          cameraToWatch = null;
        // put the camera back to the origin...
        SoCamera camera = renderArea.getSceneInteractor().getCamera();
        camera.orientation.setValue(0, 0, 0, 1);
        camera.position.setValue(0, 0, 0);
        if ( renderArea != null )
        {
          camera.viewAll(litStuff, new SbViewportRegion(renderArea.getComponent().getSize()), 2);
        }
      }
      if ( isVisible() )
        activate();
    }
  }

  /**
   * Detach this editor from the light node.
   */
  public void detach()
  {
    if ( isAttached() )
    {
      deactivate();
      if ( cameraToWatch != null )
      {
        cameraToWatch = null;
      }
      dirLight = null;
    }
  }

  /**
   * Check if this editor is attached to a light node.
   *
   * @return true if this editor is attached to a light node.
   */
  public boolean isAttached()
  {
    return (dirLight != null);
  }

  /**
   * Set new values in the light editor.
   */
  public void setLight(SoDirectionalLight newLight)
  {
    if ( dirLight != null )
    {
      lightSensor.detach();
      copyLight(dirLight, newLight); // dst, src
      lightSensor.attach(dirLight);
    }

    // set values in dirLightManip
    ignoreCallback = true; // ignore resulting manip callback
    copyLight(dirLightManip, newLight); // dst,src
    ignoreCallback = false;

    // set values in local components (colorEditor, and intensitySlider)
    updateLocalComponents();

    // invoke the callbacks
    invokeValueChanged(dirLightManip);
  }

  /**
   * Get the current light.
   */
  public SoDirectionalLight getLight()
  {
    return dirLight;
  }

  /**
   * Adds the specified listener to receive editor's events.<br>
   * If listener l is null, no exception is thrown and no action is performed.
   *
   * @param listener
   *          the editor listener
   */
  public void addListener(Listener listener)
  {
    if ( listener != null )
      editorListeners.add(listener);
  }

  /**
   * Removes the specified listener if present so that it no longer receives
   * editor's events.
   *
   * @param listener
   *          the editor listener
   * @return true if specified listener has been removed.
   */
  public boolean removeListener(Listener listener)
  {
    return editorListeners.remove(listener);
  }

  private void invokeValueChanged(SoDirectionalLight dirLight)
  {
    for ( Listener listener : editorListeners )
      listener.valueChanged(dirLight);
  }

  // Redefined here since there are two windows to deal with -
  // the color editor and the light manipulator
  @Override
  public void setVisible(boolean visible)
  {
    if ( colorEditor != null )
      colorEditor.setVisible(visible);

    super.setVisible(visible);
  }

  @Override
  public void dispose()
  {
    if ( colorEditor != null )
      colorEditor.dispose();
    super.dispose();
  }

  // copy the src light values to the dst light values
  protected void copyLight(SoDirectionalLight dst, SoDirectionalLight src)
  {
    // set values in dst
    dst.color.setValue(src.color.getValue());
    dst.intensity.setValue(src.intensity.getValue());
    dst.direction.setValue(src.direction.getValue());
  }

  protected void updateLocalComponents()
  {
    if ( colorEditor != null )
    {
      ignoreCallback = true;
      colorEditor.setColor(dirLightManip.color.getValue());
      ignoreCallback = false;
    }

    if ( intensitySlider != null )
    {
      // update the intensity to reflect the color
      ignoreCallback = true;
      intensitySlider.setSliderValue(dirLightManip.intensity.getValue());
      ignoreCallback = false;
    }
  }

  private void activate()
  {
    // make sure the components are in sync with our local light
    updateLocalComponents();

    if ( dirLight == null )
      return;

    // attach lightSensor
    if ( lightSensor.getAttachedNode() == null )
      lightSensor.attach(dirLight); // attach AFTER update

    if ( cameraToWatch != null )
    {
      cameraSensorTask.run();
      cameraSensor.attach(cameraToWatch);
    }
  }

  private void deactivate()
  {
    lightSensor.detach();
    cameraSensor.detach();
  }

  /**
   * Defines an object which listens for light editor's events.<br>
   * This object will be notified when light has changed.
   *
   */
  public static class Listener
  {
    /**
     * Invoked when light has changed.
     *
     * @param dirLight
     *          edited light
     */
    public void valueChanged(SoDirectionalLight dirLight)
    {};
  }

  // Sensor callbacks
  class CameraSensorTask implements Runnable
  {
    @Override
    public void run()
    {
      if ( cameraToWatch != null )
      {
        SbRotation newRot = cameraToWatch.orientation.getValue();
        SoCamera camera = renderArea.getSceneInteractor().getCamera();
        camera.orientation.setValue(newRot);
        camera.viewAll(litStuff, new SbViewportRegion(renderArea.getComponent().getSize()), 2);
      }
    }
  }

  class LightSensorTask implements Runnable
  {
    @Override
    public void run()
    {
      if ( !isVisible() )
        return;

      // set values in dirLightManip
      ignoreCallback = true; // ignore resulting manip callback
      copyLight(dirLightManip, dirLight); // dst, src
      ignoreCallback = false;

      updateLocalComponents();

      // invoke the callabcks
      invokeValueChanged(dirLightManip);
    }
  }

  class DirLightManipCB extends SoDraggerCB
  {
    @Override
    public void invoke(SoDragger dragger)
    {
      if ( ignoreCallback )
        return;

      // update the attached light, if it exists
      if ( dirLight != null )
      {
        lightSensor.detach();
        dirLight.direction.setValue(dirLightManip.direction.getValue());
        lightSensor.attach(dirLight);
      }

      // invoke the callbacks
      invokeValueChanged(dirLightManip);
    }
  }

  class ListenIntensitySlider extends SliderPanel.Listener
  {

    @Override
    public void stateChanged(float value)
    {
      if ( ignoreCallback )
        return;

      if ( dirLight != null )
      {
        // detach the sensor while we update the field
        lightSensor.detach();
        dirLight.intensity.setValue(value);
        lightSensor.attach(dirLight);
      }

      // update our local light
      ignoreCallback = true; // ignore resulting manip callback
      dirLightManip.intensity.setValue(value);
      ignoreCallback = false;

      // invoke the callbacks with the new values.
      invokeValueChanged(dirLightManip);
    }
  }

  class ColorEditorListener extends ColorEditor.Listener
  {
    @Override
    public void valueChanged(SbColor rgbColor)
    {
      if ( ignoreCallback )
        return;

      if ( dirLight != null )
      {
        // detach the sensor while we update the field
        lightSensor.detach();
        dirLight.color.setValue(rgbColor);
        lightSensor.attach(dirLight);
      }

      // update our local light
      ignoreCallback = true; // ignore resulting manip callback
      dirLightManip.color.setValue(rgbColor);
      ignoreCallback = false;

      // invoke the callbacks with the new values
      invokeValueChanged(dirLightManip);
    }
  }

  class ListenVisibility extends ComponentAdapter
  {
    @Override
    public void componentShown(ComponentEvent e)
    {
      activate();
    }

    @Override
    public void componentHidden(ComponentEvent e)
    {
      deactivate();
    }
  }

  class ListenMenuItem implements ActionListener
  {
    private int id;

    public ListenMenuItem(int id)
    {
      this.id = id;
    }

    @Override
    public void actionPerformed(ActionEvent e)
    {
      switch ( id )
      {
      case k_COLOR_EDITOR :
        if ( colorEditor == null )
        {
          colorEditor = new ColorEditor();
          colorEditor.setCurrentSliders(ColorEditor.NONE);
          colorEditor.addListener(new ColorEditorListener());
        }
        colorEditor.setVisible(true);
        break;
      case k_CLOSE :
        setVisible(false);
        break;
      }
    }
  }
}
